package grobit.bencode

/**
 * All data passed to encode methods must be treatable as BigInteger, String, List or Map
 */
class Bencoder {

    static encode(BigInteger val) {
        return "i${val}e"
    }

    static encode(String val) {
        return String.format('%d:%s', val.length(), val)
    }

    static encode(List val) {
        StringBuilder builder = new StringBuilder('l')
        val.each {
            builder.append(encode(it))
        }
        builder.append('e')
        return builder.toString()
    }

    static encode(Map val) {
        StringBuilder builder = new StringBuilder('d')
        val.each { key, value ->
            builder.append(encode(String.valueOf(key)) + encode(value))
        }
        builder.append('e')
        return builder.toString()
    }

    static decodeNext(InputStream inputStream) {
        String indicator = peek(inputStream)
        switch(indicator) {
            case ~/[0-9]/:
                return decodeString(inputStream)
            case 'i':
                return decodeInt(inputStream)
            case 'l':
                return decodeList(inputStream)
            case 'd':
                return decodeMap(inputStream)
            default:
                throw new Exception("Out-of-sync: '$indicator'")
        }
    }

    private static String peek(InputStream inputStream) {
        inputStream.mark(1)
        byte val = inputStream.read()
        inputStream.reset()
        return val as char
    }

    private static decodeInt(InputStream inputStream) {
        assert inputStream.read() as char == 'i'
        return decodeTerminatedInt(inputStream, 'e' as char)
    }

    private static decodeString(InputStream inputStream) {
        int length = decodeTerminatedInt(inputStream, ':' as char)
        byte[] data = new byte[length]
        inputStream.read(data)
        return new String(data, 'ISO-8859-1')
    }

    private static decodeList(InputStream inputStream) {
        assert inputStream.read() as char == 'l'
        List values = []
        while(peek(inputStream) != 'e') {
            values += decodeNext(inputStream)
        }
        assert inputStream.read() as char == 'e'
        return values
    }

    private static decodeMap(InputStream inputStream) {
        assert inputStream.read() as char == 'd'
        Map map = [:]
        while(peek(inputStream) != 'e') {
            map.put(decodeString(inputStream), decodeNext(inputStream))
        }
        assert inputStream.read() as char == 'e'
        return map
    }

    private static decodeTerminatedInt(InputStream inputStream, char terminator) {
        StringBuilder s = new StringBuilder()
        char b
        while((b = inputStream.read()) != terminator) {
            s.append(b)
        }
        // There is no theoretical limit on integer sizes in Bencoding
        return new BigInteger(s.toString())
    }
}
